/*
 * This file (ScriptPanel.java) is protected by the GNU GPL licence (v2). 
 * Please read the license.txt attached for further informations. 
 * 
 */
package org.algoristes.alkwarel.gui;

import java.awt.Color;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextPane;
import javax.swing.JToolBar;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.filechooser.FileFilter;

import org.algoristes.alkwarel.script.ScriptParser;
import org.algoristes.alkwarel.utils.Constants;
import org.algoristes.alkwarel.utils.Log;
import org.algoristes.alkwarel.utils.LogListener;

/**
 * An interface to write script files, load and save them
 * 
 * @author Xavier Gouchet
 * 
 */
public class ScriptPanel extends JPanel implements Constants, GUIConstants,
		ActionListener {

	private static final long serialVersionUID = V_SERIAL_UID;

	// GUI Members
	private JToolBar mScriptToolBar;
	private JSplitPane mSplitter;
	private JScrollPane mHistoryScroll;
	private JScrollPane mInputScroll;
	private JTextPane mHistoryText;
	private JTextPane mInputText;

	// Dialogs
	private final JFileChooser mScriptFileChooser = new JFileChooser();

	// Script Management
	private String mCurrentScriptFile;
	private boolean mCurrentScriptModified;

	/**
	 * Standard Constructor
	 */
	public ScriptPanel() {
		// Initialise GUI
		setLayout(new GridBagLayout());
		GridBagConstraints gbc = new GridBagConstraints();
		gbc.gridx = 0;
		gbc.gridy = 0;
		gbc.weightx = 1;
		gbc.weighty = 0;
		gbc.fill = GridBagConstraints.BOTH;

		add(getScriptToolBar(), gbc);

		gbc.gridy = 1;
		gbc.weighty = 1;
		add(getSplitter(), gbc);

		// Initialise other members
		mCurrentScriptFile = null;
		mCurrentScriptModified = false;

		mScriptFileChooser.setAcceptAllFileFilterUsed(false);
		mScriptFileChooser.addChoosableFileFilter(new FileFilter() {
			public boolean accept(File f) {
				if (f.isDirectory())
					return true;

				String extension = getExtension(f);
				if (extension != null)
					return (extension.equals(SCRIPT_FILE_EXT));

				return false;
			}

			public String getDescription() {
				return (SCRIPT_FILE_NAME + " (*." + SCRIPT_FILE_EXT + ")");
			}

			public String getExtension(File f) {
				String s = f.getName();
				int i = s.lastIndexOf('.');

				if (i > 0 && i < s.length() - 1)
					return s.substring(i + 1).toLowerCase();
				return null;
			}

		});

		Log.verbose("Ready to start");
	}

	private JSplitPane getSplitter() {
		if (mSplitter == null) {
			mSplitter = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
					getHistoryScroll(), getInputScroll());
			mSplitter.setDividerSize(5);
			mSplitter.setResizeWeight(0.5);
			mSplitter.setDividerLocation(50);
		}
		return mSplitter;
	}

	private JScrollPane getHistoryScroll() {
		if (mHistoryScroll == null) {
			mHistoryScroll = new JScrollPane(getHistoryText());
		}
		return mHistoryScroll;
	}

	/**
	 * Returns the history JTextPane. If it doesn't exist, create it first.
	 * 
	 * @return the main panel
	 */
	public JTextPane getHistoryText() {
		if (mHistoryText == null) {
			mHistoryText = new JTextPane();
			mHistoryText.setEditable(false);
			mHistoryText.setBackground(Color.LIGHT_GRAY);
			mHistoryText.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
			Log.addLogListener(new LogListener() {
				public void messageLoggedEvent(String msg) {
					mHistoryText.setText(mHistoryText.getText() + "\n" + msg);
				}
			});
		}
		return mHistoryText;
	}

	private JScrollPane getInputScroll() {
		if (mInputScroll == null) {
			mInputScroll = new JScrollPane(getInputText());
			mInputScroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
			mInputScroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
		}
		return mInputScroll;
	}

	/**
	 * Returns the input JTextPane. If it doesn't exist, create it first.
	 * 
	 * @return the main panel
	 */
	public JTextPane getInputText() {
		if (mInputText == null) {
			mInputText = new ScriptInput();
			// TODO disable wordwrap
			mInputText.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14));
			mInputText.getDocument().addDocumentListener(
					new DocumentListener() {

						public void removeUpdate(DocumentEvent e) {
							mCurrentScriptModified = true;
						}

						public void insertUpdate(DocumentEvent e) {
							mCurrentScriptModified = true;
						}

						public void changedUpdate(DocumentEvent e) {
							mCurrentScriptModified = true;
						}
					});

			// TODO add syntax coloration
		}
		return mInputText;
	}

	/**
	 * Returns the script panel JToolBar. If it doesn't exist, create it first.
	 * 
	 * @return the main panel
	 */
	public JToolBar getScriptToolBar() {
		if (mScriptToolBar == null) {
			mScriptToolBar = new JToolBar("Alkwarel Tool Box");
			mScriptToolBar.setFloatable(false);
			mScriptToolBar.setRollover(true);

			addToolBarButton(CMD_NEW, "New", "new.png");
			addToolBarButton(CMD_CLEAR, "Clear History", "new.png");
			addToolBarButton(CMD_SAVE, "Save", "save.png");
			addToolBarButton(CMD_LOAD, "Load", "load.png");
			addToolBarButton(CMD_RUN, "Run", "run.png");
		}
		return mScriptToolBar;
	}

	private void addToolBarButton(String action, String text, String image) {
		JButton button = new JButton();
		button.setActionCommand(action);

		button.addActionListener(this);
		button.setMargin(new Insets(4, 4, 4, 4));
		button.setBorder(null);

		if ((image == null) || (image.length() == 0))
			button.setText(text);
		else {
			ImageIcon icn = new ImageIcon("icons/" + image);
			button.setIcon(icn);
		}

		mScriptToolBar.add(button);
	}

	/**
	 * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
	 */
	public void actionPerformed(ActionEvent event) {
		Object source = event.getSource();
		String command = event.getActionCommand();

		if (source.getClass() == JButton.class) {
			if (command.equals(CMD_NEW))
				if (saveModified())
					clearScript();
			if (command.equals(CMD_CLEAR))
				getHistoryText().setText("");
			if (command.equals(CMD_SAVE))
				saveCurrentScript();
			if (command.equals(CMD_LOAD))
				if (saveModified())
					loadScript();
			if (command.equals(CMD_RUN)) {
				ScriptParser parser = new ScriptParser(0);
				parser.runScript(mInputText.getText());
			}
		}
	}

	/**
	 * Checks if the input has been modified since load or last save. In such
	 * case, ask the user to save or loose the modifications.
	 * 
	 * @return true if the input text can be replaced, false if the operation
	 *         was canceled by user
	 */
	public boolean saveModified() {
		if (!mCurrentScriptModified)
			return true;

		int result = JOptionPane.showConfirmDialog(null,
				"Your script has been modified, do you want to save it ?",
				"Save ? ", JOptionPane.YES_NO_CANCEL_OPTION);
		switch (result) {
		case JOptionPane.OK_OPTION:
			return saveCurrentScript();
		case JOptionPane.NO_OPTION:
			return true;
		case JOptionPane.CANCEL_OPTION:
		default:
			return false;
		}
	}

	/**
	 * Clears the current script (equivalent to file new)
	 */
	public void clearScript() {
		mInputText.setText("");
		mCurrentScriptModified = false;
	}

	/**
	 * Opens a dialog to determine the path to load from, then loads to the
	 * current script.
	 */
	public void loadScript() {
		int res = mScriptFileChooser.showOpenDialog(this);
		switch (res) {
		case JFileChooser.APPROVE_OPTION:
			loadScriptFromFile(mScriptFileChooser.getSelectedFile());
		case JFileChooser.CANCEL_OPTION:
		default:
			break;
		}
	}

	/**
	 * Loads a script from the given file
	 * 
	 * @param f
	 *            the file from which the script is loaded
	 * 
	 */
	public void loadScriptFromFile(File f) {
		if (f == null)
			return;

		try {
			loadScriptFromFileUnsafe(f);
			mCurrentScriptModified = false;
		} catch (FileNotFoundException e) {
			Log.severe("File not found, or file is a folder");
		} catch (IOException e) {
			Log.severe("I/O error");
		} catch (SecurityException e) {
			Log.severe("Security manager prevents to read the file");
		}

	}

	/**
	 * @param f
	 *            the file from which the script is loaded
	 * @throws IOException
	 *             if an I/O error occurs
	 * @throws FileNotFoundException
	 *             if the file exists but is a directory rather than a regular
	 *             file, does not exist but cannot be created, or cannot be
	 *             opened for any other reason
	 * @throws SecurityException
	 *             if a security manager exists and its checkWrite method denies
	 *             read access to the file
	 */
	public void loadScriptFromFileUnsafe(File f) throws IOException,
			FileNotFoundException, SecurityException {
		Reader inReader = null;
		InputStream inStream = null;
		inStream = new FileInputStream(f);
		inReader = new InputStreamReader(inStream);
		BufferedReader buffer = new BufferedReader(inReader);
		StringBuffer dest = new StringBuffer();
		String line;
		while ((line = buffer.readLine()) != null)
			dest.append(line + "\n");

		buffer.close();
		inReader.close();
		inStream.close();

		mInputText.setText(dest.toString());
		mCurrentScriptFile = f.getPath();
		Log.verbose("Loaded file : " + f.getPath());
	}

	/**
	 * Saves the current Script to the last opened / saved path. If such a past
	 * doesn't exist, {@link #saveCurrentScriptAs()} is called instead.
	 * 
	 * @return true if the file has been saved successfully
	 * 
	 */
	public boolean saveCurrentScript() {
		if ((mCurrentScriptFile == null) || (mCurrentScriptFile.length() == 0))
			return saveCurrentScriptAs();
		else {
			File f = new File(mCurrentScriptFile);
			return saveCurrentScriptToFile(f);
		}
	}

	/**
	 * Opens a dialog to determine the path to save to, then saves the current
	 * script.
	 * 
	 * @return true if the script has been saved succesfully
	 */
	public boolean saveCurrentScriptAs() {
		int res = mScriptFileChooser.showOpenDialog(this);
		switch (res) {
		case JFileChooser.APPROVE_OPTION:
			return saveCurrentScriptToFile(mScriptFileChooser.getSelectedFile());
		case JFileChooser.CANCEL_OPTION:
		default:
			return false;
		}

	}

	/**
	 * Saves the current script to the given path
	 * 
	 * @param f
	 *            the file to save the script to
	 * @return true if the script has been saved succesfully
	 */
	public boolean saveCurrentScriptToFile(File f) {
		if (f == null)
			return false;

		if (!f.getPath().endsWith("." + SCRIPT_FILE_EXT))
			f = new File(f.getPath() + "." + SCRIPT_FILE_EXT);
		try {
			saveCurrentScriptToFileUnsafe(f);
			mCurrentScriptModified = false;
		} catch (FileNotFoundException e) {
			Log.severe("File not found, or file is a folder");
			return false;
		} catch (IOException e) {
			Log.severe("I/O error");
			return false;
		} catch (SecurityException e) {
			Log.severe("Security manager prevents to write to the file");
			return false;
		}
		return true;
	}

	/**
	 * Saves the current script to the given path without catching exceptions
	 * neither testing the validity of the input file
	 * 
	 * @param f
	 *            the file to save the script to
	 * @throws IOException
	 *             if an I/O error occurs
	 * @throws FileNotFoundException
	 *             if the file exists but is a directory rather than a regular
	 *             file, does not exist but cannot be created, or cannot be
	 *             opened for any other reason
	 * @throws SecurityException
	 *             if a security manager exists and its checkWrite method denies
	 *             write access to the file
	 */
	public void saveCurrentScriptToFileUnsafe(File f) throws IOException,
			FileNotFoundException, SecurityException {
		Writer outWriter = null;
		OutputStream outStream = null;
		String source = mInputText.getText();
		outStream = new FileOutputStream(f);
		outWriter = new OutputStreamWriter(outStream);
		outWriter.write(source);
		outWriter.close();
		mCurrentScriptFile = f.getPath();
		Log.verbose("Script saved to " + mCurrentScriptFile);
	}
}
